%% Author: vivimice
%% Created: Feb 3, 2012
%% Description: utilities for telnet io
-module(tio_manager).

%%
%% Include files
%%
-include("telnet.hrl").
-include("keys.hrl").
-include("tchar.hrl").
-include("tstring.hrl").

-define(ESC, 16#1b).

%%
%% Exported Functions
%%
-export([init/1, close/1, read/1, write/2, format/3, format_s/2, format_w/2,
		 set_cursor/3, move_cursor/3, move_cursor_horizontally/2, move_cursor_vertically/2,
		 write_newline/1, flush/1, bell/1]).

%%
%% API Functions
%%
init(Context) ->
	% default window size
	context:set_window_size(Context, {24, 80}),
	
	% start receivers and senders
	Receiver = tio_receiver:start(context:get_socket(Context), fun telnet_option_handler/2, [Context]),
	context:set_receiver(Context, Receiver),
	
	Sender = tio_sender:start(context:get_socket(Context)),
	context:set_sender(Context, Sender),
	
	% init telnet options
	init_telnet_options(Sender),
	ok.

close(Context) ->
	tio_sender:stop(context:get_sender(Context)),
	tio_receiver:stop(context:get_receiver(Context)).

%% read one tchar or one key from client
read(Context) ->
	Receiver = context:get_receiver(Context),
	case tio_receiver:read_byte(Receiver) of
		Byte when is_integer(Byte) ->
			if 
				(Byte band 16#80) > 0 ->
					% creater than 127, multibyte chars, wait for the next one
					NextByte = tio_receiver:read_byte(Receiver),
					{tchar, tchar:new_mb(<<Byte, NextByte>>)};
				Byte == 16#1b ->
					% escape sequence
					case read_escape_sequence(Receiver) of
						undef -> 
							read(Context);	% discard unknown control, re-try reading
						Control ->
							{control, Control}
					end;
				
				Byte == 16#0d ->
					{control, ?KEY_ENTER};
				
				Byte == 16#00 ->
					% in rfc standard, cr is \r\0, but in fterm, just /r is sent,
					% so just ignore \0 to maintain compability
					?MODULE:read(Context);
				
				Byte == 16#08 orelse Byte == 16#7f ->
					{control, ?KEY_BACKSPACE};
				true ->
					% ascii char
					{tchar, tchar:new_ansi(Byte)}
			end;
		
		{error, Reason} ->
			erlang:exit(Reason)
	end.

%% write binary, tchar, tstring to client
write(_Context, []) ->
	ok;
write(Context, Binary) when erlang:is_binary(Binary)->
	tio_sender:write_bytes(context:get_sender(Context), Binary);
write(Context, TChar) when ?IS_TCHAR(TChar) ->
	write(Context, tchar:to_binary(TChar));
write(Context, TString) when ?IS_TSTRING(TString) ->
	write(Context, tstring:to_binary(TString)).

%% write newline ("\r\n") to client, will auto flush buffer.
write_newline(Context) ->
	tio_sender:write_bytes_nb(context:get_sender(Context), <<"\r\n">>).

%% formatted output, will auto flush buffer.
format(Context, Format, Args) when is_list(Args) ->
	% convert all tchar into binary
	ConvertArgFun = 
		fun (Arg) ->
				 if 
					 ?IS_TCHAR(Arg) ->
						 tchar:to_binary(Arg);
					 ?IS_TSTRING(Arg) ->
						 tstring:to_binary(Arg);
					 true ->
						 Arg
				 end
		end,
	NewArgs = [ConvertArgFun(X) || X <- Args],
	FormattedString = io_lib:format(Format, NewArgs),
	Sender = context:get_sender(Context),
	tio_sender:write_bytes(Sender, list_to_binary(FormattedString)),
	tio_sender:flush(Sender);
format(_Context, _Format, _Args) ->
	erlang:error(badarg).

%% formatted output as string
format_s(Context, String) ->
	format(Context, "~s", String).

%% formatted output as standard erlang syntax
format_w(Context, Term) ->
	format(Context, "~w", Term).

flush(Context) ->
	tio_sender:flush(context:get_sender(Context)).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% cursor management methods
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% ring bell
bell(Context) ->
	tio_sender:write_bytes(context:get_sender(Context), <<$\x07>>).

%% set cursor position
set_cursor(Context, Row, Column) ->
 	tio_sender:write_bytes(context:get_sender(Context), list_to_binary([?ESC, "[", integer_to_list(Row), ";", integer_to_list(Column), "H"])),
 	ok.

%% move cursor position relatively
move_cursor(Context, 0, NumColumns) ->
	move_cursor_horizontally(Context, NumColumns);
move_cursor(Context, NumRows, 0) ->
	move_cursor_vertically(Context, NumRows);
move_cursor(Context, NumRows, NumColumns) ->
	move_cursor_horizontally(Context, NumColumns),
	move_cursor_vertically(Context, NumRows),
	ok.

%% move cursor position relatively
move_cursor_horizontally(_Context, 0) ->
	ok;
move_cursor_horizontally(Context, NumColumns) when NumColumns > 0 ->
	tio_sender:write_bytes(context:get_sender(Context), list_to_binary([?ESC, "[", integer_to_list(NumColumns), "C"])),
 	ok;
move_cursor_horizontally(Context, NumColumns) ->
	tio_sender:write_bytes(context:get_sender(Context), list_to_binary([?ESC, "[", integer_to_list(-NumColumns), "D"])),
 	ok.

move_cursor_vertically(_Context, 0) ->
 	ok;
move_cursor_vertically(Context, NumRows) when NumRows > 0 ->
	tio_sender:write_bytes(context:get_sender(Context), list_to_binary([?ESC, "[", integer_to_list(NumRows), "B"])),
 	ok;
move_cursor_vertically(Context, NumRows) ->
	tio_sender:write_bytes(context:get_sender(Context), list_to_binary([?ESC, "[", integer_to_list(-NumRows), "A"])),
 	ok.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%
%% Local Functions
%%
init_telnet_options(Sender) ->
	tio_sender:send_telnet_option(Sender, ?WILL, ?TELNET_OPT_ECHO),
	tio_sender:send_telnet_option(Sender, ?DONT, ?TELNET_OPT_LINE_MODE),
	tio_sender:send_telnet_option(Sender, ?WILL, ?TELNET_OPT_SUPPRESS_GO_AHEAD),
	tio_sender:write_bytes(Sender, <<"\x1b[?7l">>), % disable auto wrap mode
	tio_sender:flush(Sender),
	ok.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% telnet option handler
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% disable go ahead
telnet_option_handler({option, ?WILL, ?TELNET_OPT_SUPPRESS_GO_AHEAD}, Context) ->
	Sender = context:get_sender(Context),
	tio_sender:send_telnet_option(Sender, ?DO, ?TELNET_OPT_SUPPRESS_GO_AHEAD);

telnet_option_handler({option, ?DO, ?TELNET_OPT_SUPPRESS_GO_AHEAD}, _Context) ->
	ok;	% no response to prevent infinite do/will loop

% disable local echo
telnet_option_handler({option, ?DO, ?TELNET_OPT_ECHO}, Context) ->
	Sender = context:get_sender(Context),
	tio_sender:send_telnet_option(Sender, ?WILL, ?TELNET_OPT_ECHO);

% get termtype
telnet_option_handler({option, ?WILL, ?TELNET_OPT_TERM_TYPE}, Context) ->
	Sender = context:get_sender(Context),
	tio_sender:send_telnet_suboption(Sender, ?TELNET_OPT_TERM_TYPE, <<?SEND>>);

% disable line mode
telnet_option_handler({option, ?WILL, ?TELNET_OPT_LINE_MODE}, Context) ->
	Sender = context:get_sender(Context),
	tio_sender:send_telnet_option(Sender, ?DONT, ?TELNET_OPT_LINE_MODE);

% negotiate about window size, see RFC 1073 
telnet_option_handler({option, ?WILL, ?TELNET_OPT_WINDOW_SIZE}, Context) ->
	Sender = context:get_sender(Context),
	tio_sender:send_telnet_option(Sender, ?DO, ?TELNET_OPT_WINDOW_SIZE);

% negotiate about window size, see RFC 1073 
telnet_option_handler({suboption, <<?TELNET_OPT_WINDOW_SIZE, MaxColumns:16/integer-big, MaxRows:16/integer-big>>}, Context) ->
	logger:debug("window size: ~p x ~p", [MaxColumns, MaxRows]),
	context:set_window_size(Context, {MaxRows, MaxColumns});

telnet_option_handler({option, Command, Option}, _Context) ->
	logger:debug("Option received: ~p, ~p", [Command, Option]);

telnet_option_handler({suboption, SubOptionBin}, _Context) ->
	logger:debug("SubOption received: ~w", [SubOptionBin]).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% read escape sequence - FSMs
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
read_escape_sequence(Receiver) ->
	loop_escape_sequence(Receiver, [tio_receiver:read_byte(Receiver)]).

%%%%%%%%%%%%%%%%%%%%%%%

loop_escape_sequence(_Receiver, [16#1b]) ->
	?KEY_ESC;
loop_escape_sequence(_Receiver, Seq) when Seq == "[B"; Seq == "OB" ->
	?KEY_DOWN;
loop_escape_sequence(_Receiver, Seq) when Seq == "[C"; Seq == "OC" ->
	?KEY_RIGHT;
loop_escape_sequence(_Receiver, Seq) when Seq == "[A"; Seq == "OA" ->
	?KEY_UP;
loop_escape_sequence(_Receiver, Seq) when Seq == "[D"; Seq == "OD" ->
	?KEY_LEFT;
loop_escape_sequence(_Receiver, "[1~") ->
	?KEY_HOME;
loop_escape_sequence(_Receiver, "[2~") ->
	?KEY_INSERT;
loop_escape_sequence(_Receiver, "[3~") ->
	?KEY_DELETE;
loop_escape_sequence(_Receiver, "[4~") ->
	?KEY_END;
loop_escape_sequence(_Receiver, "[5~") ->
	?KEY_PAGE_UP;
loop_escape_sequence(_Receiver, "[6~") ->
	?KEY_PAGE_DOWN;

loop_escape_sequence(Receiver, Seq) when Seq == "["; Seq == "O" ->
	loop_escape_sequence(Receiver, Seq ++ [tio_receiver:read_byte(Receiver)]);

loop_escape_sequence(Receiver, "[" ++ Seq) when erlang:length(Seq) == 1 ->
	loop_escape_sequence(Receiver, "[" ++ Seq ++ [tio_receiver:read_byte(Receiver)]);

%%%%%%%%%%%%%%%%%%%%%%%

%% esc seq error fall-through
loop_escape_sequence(_Receiver, Seq) ->
	logger:warn("Unknown escape sequence: ESC ~w", [Seq]),
	undef.